--[[
	Name: AceModuleCore-2.0
	Revision: $Rev: 17998 $
	Developed by: The Ace Development Team (http://www.wowace.com/index.php/The_Ace_Development_Team)
	Inspired By: Ace 1.x by Turan (turan@gryphon.com)
	Website: http://www.wowace.com/
	Documentation: http://www.wowace.com/index.php/AceModuleCore-2.0
	SVN: http://svn.wowace.com/root/trunk/Ace2/AceModuleCore-2.0
	Description: Mixin to provide a module system so that modules or plugins can
	use an addon as its core.
	Dependencies: AceLibrary, AceOO-2.0, AceAddon-2.0, AceEvent-2.0 (optional), Compost-2.0 (optional)
]]

local MAJOR_VERSION = "AceModuleCore-2.0"
local MINOR_VERSION = "$Revision: 17998 $"

if not AceLibrary then error(MAJOR_VERSION .. " requires AceLibrary") end
if not AceLibrary:IsNewVersion(MAJOR_VERSION, MINOR_VERSION) then return end

if loadstring("return function(...) return ... end") and AceLibrary:HasInstance(MAJOR_VERSION) then return end -- lua51 check
if not AceLibrary:HasInstance("AceOO-2.0") then error(MAJOR_VERSION .. " requires AceOO-2.0") end

local function safecall(func,a,b,c,d,e,f,g)
	local success, err = pcall(func,a,b,c,d,e,f,g)
	if not success then geterrorhandler()(err) end
end

local table_setn
do
	local version = GetBuildInfo()
	if string.find(version, "^2%.") then
		-- 2.0.0
		table_setn = function() end
	else
		table_setn = table.setn
	end
end

local new, del
do
	local list = setmetatable({}, {__mode = 'k'})
	function new()
		local t = next(list)
		if t then
			list[t] = nil
			return t
		else
			return {}
		end
	end
	function del(t)
		for k in pairs(t) do
			t[k] = nil
		end
		table_setn(t, 0)
		list[t] = true
		return nil
	end
end

local AceOO = AceLibrary:GetInstance("AceOO-2.0")
local AceModuleCore = AceOO.Mixin {
	"NewModule",
	"HasModule",
	"GetModule",
	"IsModule",
	"IterateModules",
	"SetModuleMixins", 
	"SetModuleClass",
	"IsModuleActive",
	"ToggleModuleActive"
}
local AceEvent

local Compost = AceLibrary:HasInstance("Compost-2.0") and AceLibrary("Compost-2.0")

local function getlibrary(lib)
	if type(lib) == "string" then
		return AceLibrary(lib)
	else
		return lib
	end
end

local tmp
function AceModuleCore:NewModule(name, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20)
	if not self.modules then
		AceModuleCore:error("CreatePrototype() must be called before attempting to create a new module.", 2)
	end
	AceModuleCore:argCheck(name, 2, "string")
	if string.len(name) == 0 then
		AceModuleCore:error("Bad argument #2 to `NewModule`, string must not be empty")
	end
	if self.modules[name] then
		AceModuleCore:error("The module %q has already been registered", name)
	end
	
	if not tmp then
		tmp = {}
	end
	if a1 then table.insert(tmp, a1)
		if a2 then table.insert(tmp, a2)
			if a3 then table.insert(tmp, a3)
				if a4 then table.insert(tmp, a4)
					if a5 then table.insert(tmp, a5)
						if a6 then table.insert(tmp, a6)
							if a7 then table.insert(tmp, a7)
								if a8 then table.insert(tmp, a8)
									if a9 then table.insert(tmp, a9)
										if a10 then table.insert(tmp, a10)
											if a11 then table.insert(tmp, a11)
												if a12 then table.insert(tmp, a12)
													if a13 then table.insert(tmp, a13)
														if a14 then table.insert(tmp, a14)
															if a15 then table.insert(tmp, a15)
																if a16 then table.insert(tmp, a16)
																	if a17 then table.insert(tmp, a17)
																		if a18 then table.insert(tmp, a18)
																			if a19 then table.insert(tmp, a19)
																				if a20 then table.insert(tmp, a20)
																				end end end end end end end end end end end end end end end end end end end end
																				for k,v in ipairs(tmp) do
																					tmp[k] = getlibrary(v)
																				end
																				
																				if self.moduleMixins then
																					for _,mixin in ipairs(self.moduleMixins) do
																						local exists = false
																						for _,v in ipairs(tmp) do
																							if mixin == v then
																								exists = true
																								break
																							end
																						end
																						if not exists then
																							table.insert(tmp, mixin)
																						end
																					end
																				end
																				
																				local module = AceOO.Classpool(self.moduleClass, unpack(tmp)):new(name)
																				self.modules[name] = module
																				module.name = name
																				module.title = name
																				
																				AceModuleCore.totalModules[module] = self
																				
																				if AceEvent then
																					AceEvent:TriggerEvent("Ace2_ModuleCreated", module)
																				end
																				
																				for k in pairs(tmp) do
																					tmp[k] = nil
																				end
																				table_setn(tmp, 0)
																				return module
end

function AceModuleCore:HasModule(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20)
	if a1 then if not self.modules[a1] then return false end
		if a2 then if not self.modules[a2] then return false end
			if a3 then if not self.modules[a3] then return false end
				if a4 then if not self.modules[a4] then return false end
					if a5 then if not self.modules[a5] then return false end
						if a6 then if not self.modules[a6] then return false end
							if a7 then if not self.modules[a7] then return false end
								if a8 then if not self.modules[a8] then return false end
									if a9 then if not self.modules[a9] then return false end
										if a10 then if not self.modules[a10] then return false end
											if a11 then if not self.modules[a11] then return false end
												if a12 then if not self.modules[a12] then return false end
													if a13 then if not self.modules[a13] then return false end
														if a14 then if not self.modules[a14] then return false end
															if a15 then if not self.modules[a15] then return false end
																if a16 then if not self.modules[a16] then return false end
																	if a17 then if not self.modules[a17] then return false end
																		if a18 then if not self.modules[a18] then return false end
																			if a19 then if not self.modules[a19] then return false end
																				if a20 then if not self.modules[a20] then return false end
																				end end end end end end end end end end end end end end end end end end end end
																				
																				return true
end

function AceModuleCore:GetModule(name)
	if not self.modules then
		AceModuleCore:error("Error initializing class.  Please report error.")
	end
	if not self.modules[name] then
		AceModuleCore:error("Cannot find module %q.", name)
	end
	return self.modules[name]
end

function AceModuleCore:IsModule(module)
	if self == AceModuleCore then
		return AceModuleCore.totalModules[module]
	else
		for k,v in pairs(self.modules) do
			if v == module then
				return true
			end
		end
		return false
	end
end

function AceModuleCore:IterateModules()
	local t = new()
	for k in pairs(self.modules) do
		table.insert(t, k)
	end
	table.sort(t)
	local i = 0
	return function()
		i = i + 1
		local x = t[i]
		if x then
			return x, self.modules[x]
		else
			t = del(t)
			return nil
		end
	end, nil, nil
end

function AceModuleCore:SetModuleMixins(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20)
	if self.moduleMixins then
		AceModuleCore:error('Cannot call "SetModuleMixins" twice')
	elseif not self.modules then
		AceModuleCore:error("Error initializing class.  Please report error.")
	elseif next(self.modules) then
		AceModuleCore:error('Cannot call "SetModuleMixins" after "NewModule" has been called.')
	end
	
	self.moduleMixins = Compost and Compost:Acquire(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20) or {a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20}
	for k,v in ipairs(self.moduleMixins) do
		self.moduleMixins[k] = getlibrary(v)
	end
end

function AceModuleCore:SetModuleClass(class)
	class = getlibrary(class)
	AceModuleCore:assert(AceOO.inherits(class, AceOO.Class), "Bad argument #2 to `SetModuleClass' (Class expected)")
	if not self.modules then
		AceModuleCore:error("Error initializing class.  Please report error.")
	end
	if self.customModuleClass then
		AceModuleCore:error("Cannot call `SetModuleClass' twice.")
	end
	self.customModuleClass = true
	self.moduleClass = class
	self.modulePrototype = class.prototype
end

function AceModuleCore:ToggleModuleActive(module, state)
	AceModuleCore:argCheck(module, 2, "table", "string")
	AceModuleCore:argCheck(state, 3, "nil", "boolean")
	
	if type(module) == "string" then
		if not self:HasModule(module) then
			AceModuleCore:error("Cannot find module %q", module)
		end
		module = self:GetModule(module)
	else
		if not self:IsModule(module) then
			AceModuleCore:error("%q is not a module", module)
		end
	end
	
	local disable
	if state == nil then
		disable = self:IsModuleActive(module)
	else
		disable = not state
		if disable ~= self:IsModuleActive(module) then
			return
		end
	end
	
	if type(module.ToggleActive) == "function" then
		return module:ToggleActive(not disable)
	elseif AceOO.inherits(self, "AceDB-2.0") then
		if not self.db or not self.db.raw then
			AceModuleCore:error("Cannot toggle a module until `RegisterDB' has been called and `ADDON_LOADED' has been fireed.")
		end
		if type(self.db.raw.disabledModules) ~= "table" then
			self.db.raw.disabledModules = Compost and Compost:Acquire() or {}
		end
		local _,profile = self:GetProfile()
		if type(self.db.raw.disabledModules[profile]) ~= "table" then
			self.db.raw.disabledModules[profile] = Compost and Compost:Acquire() or {}
		end
		if type(self.db.raw.disabledModules[profile][module.name]) ~= "table" then
			self.db.raw.disabledModules[profile][module.name] = disable or nil
		end
		if not disable then
			if not next(self.db.raw.disabledModules[profile]) then
				if Compost then
					Compost:Reclaim(self.db.raw.disabledModules[profile])
				end
				self.db.raw.disabledModules[profile] = nil
			end
			if not next(self.db.raw.disabledModules) then
				if Compost then
					Compost:Reclaim(self.db.raw.disabledModules)
				end
				self.db.raw.disabledModules = nil
			end
		end
	else
		if type(self.disabledModules) ~= "table" then
			self.disabledModules = Compost and Compost:Acquire() or {}
		end
		self.disabledModules[module.name] = disable or nil
	end
	if AceOO.inherits(module, "AceAddon-2.0") then
		local AceAddon = AceLibrary("AceAddon-2.0")
		if not AceAddon.addonsStarted[module] then
			return
		end
	end
	if not disable then
		local current = module.class
		while true do
			if current == AceOO.Class then
				break
			end
			if current.mixins then
				for mixin in pairs(current.mixins) do
					if type(mixin.OnEmbedEnable) == "function" then
						safecall(mixin.OnEmbedEnable, mixin, module)
					end
				end
			end
			current = current.super
		end
		if type(module.OnEnable) == "function" then
			safecall(module.OnEnable, module)
		end
		if AceEvent then
			AceEvent:TriggerEvent("Ace2_AddonEnabled", module)
		end
	else
		local current = module.class
		while true do
			if current == AceOO.Class then
				break
			end
			if current.mixins then
				for mixin in pairs(current.mixins) do
					if type(mixin.OnEmbedDisable) == "function" then
						safecall(mixin.OnEmbedDisable, mixin, module)
					end
				end
			end
			current = current.super
		end
		if type(module.OnDisable) == "function" then
			safecall(module.OnDisable, module)
		end
		if AceEvent then
			AceEvent:TriggerEvent("Ace2_AddonDisabled", module)
		end
	end
	return not disable
end

function AceModuleCore:IsModuleActive(module)
	AceModuleCore:argCheck(module, 2, "table", "string")
	
	if AceModuleCore == self then
		self:argCheck(module, 2, "table")
		
		local core = AceModuleCore.totalModules[module]
		if not core then
			self:error("Bad argument #2 to `IsModuleActive'. Not a module")
		end
		return core:IsModuleActive(module)
	end
	
	if type(module) == "string" then
		if not self:HasModule(module) then
			AceModuleCore:error("Cannot find module %q", module)
		end
		module = self:GetModule(module)
	else
		if not self:IsModule(module) then
			AceModuleCore:error("%q is not a module", module)
		end
	end
	
	if type(module.IsActive) == "function" then
		return module:IsActive()
	elseif AceOO.inherits(self, "AceDB-2.0") then
		local _,profile = self:GetProfile()
		return not self.db or not self.db.raw or not self.db.raw.disabledModules or not self.db.raw.disabledModules[profile] or not self.db.raw.disabledModules[profile][module.name]
	else
		return not self.disabledModules or not self.disabledModules[module.name]
	end
end

function AceModuleCore:OnInstanceInit(target)
	if target.modules then
		AceModuleCore:error("OnInstanceInit cannot be called twice")
	end
	target.modules = Compost and Compost:Acquire() or {}
	
	target.moduleClass = AceOO.Class("AceAddon-2.0")
	target.modulePrototype = target.moduleClass.prototype
end

AceModuleCore.OnManualEmbed = AceModuleCore.OnInstanceInit

function AceModuleCore.OnEmbedProfileDisable(AceModuleCore, self, newProfile)
	if not AceOO.inherits(self, "AceDB-2.0") then
		return
	end
	local _,currentProfile = self:GetProfile()
	for k, module in pairs(self.modules) do
		if type(module.IsActive) == "function" or type(module.ToggleActive) == "function" then
			-- continue
		else
			local currentActive =  not self.db or not self.db.raw or not self.db.raw.disabledModules or not self.db.raw.disabledModules[currentProfile] or not self.db.raw.disabledModules[currentProfile][module.name]
			local newActive =  not self.db or not self.db.raw or not self.db.raw.disabledModules or not self.db.raw.disabledModules[newProfile] or not self.db.raw.disabledModules[newProfile][module.name]
			if currentActive ~= newActive then
				self:ToggleModuleActive(module)
				if not self.db.raw.disabledModules then
					self.db.raw.disabledModules = {}
				end
				if not self.db.raw.disabledModules[currentProfile] then
					self.db.raw.disabledModules[currentProfile] = {}
				end
				self.db.raw.disabledModules[currentProfile][module.name] = not currentActive or nil
			end
		end
	end
end

local function activate(self, oldLib, oldDeactivate)
	AceModuleCore = self
	
	if oldLib then
		self.totalModules = oldLib.totalModules
	end
	if not self.totalModules then
		self.totalModules = {}
	end
	
	self.super.activate(self, oldLib, oldDeactivate)
end

local function external(self, major, instance)
	if major == "Compost-2.0" then
		Compost = instance
	elseif major == "AceEvent-2.0" then
		AceEvent = instance
	end
end

AceLibrary:Register(AceModuleCore, MAJOR_VERSION, MINOR_VERSION, activate, nil, external)
AceModuleCore = AceLibrary(MAJOR_VERSION)